/*------------------------------------------------------------------------------
 * JSONLib
 * Version 0.1 Alpha
 * by Nicholas C. Zakas, http://www.nczonline.net/
 * Copyright (c) 2008 Nicholas C. Zakas. All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 *------------------------------------------------------------------------------
 */  
(function(){

    /**
     * Determines if a given value can be represented in JSON.
     * @param {Variant} value The value to check.
     * @return {Boolean} True if the value can be represented, false if not.
     * @private
     */
    function isSupportedValue(value /*:Variant*/){
        switch(typeof value){
            case "object":
                return (value === null || value instanceof JSON || value instanceof Date);
            case "string":
            case "boolean":
            case "number":
                return true;
            default:
                return false;
        }        
    }

    /**
     * Represents a JSON object.
     * @class JSON
     * @constructor
     * @param {Object} data (Optional) An object with data to add to the JSON object.
     */
    JSON = function(data /*:Object*/){    
        if (this instanceof JSON) {
            this._data = new Object();
            
            //initialize
            if (typeof data == "object" && data !== null){
                for (var prop in data){
                    if (isSupportedValue(data[prop])){
                        this._data[prop] = data[prop];
                    }
                }
            }
        } else {
            return new JSON(data);
        }    
    };

    /**
     * Parses a JSON string into JSON and JSONList objects. Based on 
     * Douglas Crockford's original JSON parser. All credit to him, and
     * as he requests, please do not use this code for evil. 
     * @param {String} text The JSON string to parse.
     * @throws SyntaxError when there's a JSON syntax error.
     * @return {JSON} The created JSON object.
     */
    JSON.parse = function(text /*:String*/) /*:JSON*/{
    
        var chars = text.split("");
        var pos = 0;
        var c = " ";

        function next() {
            return (c = chars[pos++]);
        }
        
        function read(count){
            var s = "";
            while(count){
                s += next();
                count--;
            }
            return s;
        }

        function white() {
            while (c) {
                if (c <= ' ') {
                    next();
                } else if (c == '/') {
                    switch (next()) {
                        case '/':
                            while (next() && c != '\n' && c != '\r') {}
                            break;
                        case '*':
                            next();
                            for (;;) {
                                if (c) {
                                    if (c == '*') {
                                        if (next() == '/') {
                                            next();
                                            break;
                                        }
                                    } else {
                                        next();
                                    }
                                } else {
                                    throw new SyntaxError("JSON.parse(): Unterminated comment at position " + pos + ".");
                                }
                            }
                            break;
                        default:
                            throw new SyntaxError("JSON.parse(): Unexpected character (" + c + ") found.");
                    }
                } else {
                    break;
                }
            }
        }

        function string() {
            var i, s = '', t, u;

            if (c == '"') {
outer:          while (next()) {
                    if (c == '"') {
                        next();
                        return s;
                    } else if (c == '\\') {
                        switch (next()) {
                        case 'b':
                        case 'f':
                        case 'n':
                        case 'r':
                        case 't':
                            s += "\'" + c;
                            break;
                        case 'u':
                            u = 0;
                            for (i = 0; i < 4; i += 1) {
                                t = parseInt(next(), 16);
                                if (!isFinite(t)) {
                                    break outer;
                                }
                                u = u * 16 + t;
                            }
                            s += String.fromCharCode(u);
                            break;
                        default:
                            s += c;
                        }
                    } else {
                        s += c;
                    }
                }
            }
            throw new SyntaxError("JSON.parse(): Invalid string at position " + pos + ".");
        }

        function array() {
            var a = new JSONList();

            if (c == '[') {
                next();
                white();
                if (c == ']') {
                    next();
                    return a;
                }
                while (c) {
                    a.push(value());
                    white();
                    if (c == ']') {
                        next();
                        return a;
                    } else if (c != ',') {
                        break;
                    }
                    next();
                    white();
                }
            }
            throw new SyntaxError("JSON.parse(): Invalid array at position " + pos + ".");
        }

        function object() {
            var o = new JSON();
            var k;

            if (c == '{') {
                next();
                white();
                if (c == '}') {
                    next();
                    return o;
                }
                while (c) {
                    k = string();
                    white();
                    if (c != ':') {
                        break;
                    }
                    next();
                    o.put(k, value());
                    white();
                    if (c == '}') {
                        next();
                        return o;
                    } else if (c != ',') {
                        break;
                    }
                    next();
                    white();
                }
            }
            throw new SyntaxError("JSON.parse(): Invalid object at position " + pos + ".");
        }

        function number() {
            var n = '', v;
            if (c == '-') {
                n = '-';
                next();
            }
            while (c >= '0' && c <= '9') {
                n += c;
                next();
            }
            if (c == '.') {
                n += '.';
                while (next() && c >= '0' && c <= '9') {
                    n += c;
                }
            }
            if (c == 'e' || c == 'E') {
                n += 'e';
                next();
                if (c == '-' || c == '+') {
                    n += c;
                    next();
                }
                while (c >= '0' && c <= '9') {
                    n += c;
                    next();
                }
            }
            v = +n;
            if (!isFinite(v)) {
                throw new SyntaxError("JSON.parse(): Bad number (" + v + ")");
            } else {
                return v;
            }
        }

        function word() {
            switch (c) {
                case 't':
                    if (read(3) == "rue") {
                        next();
                        return true;
                    }
                    break;
                case 'f':
                    if (read(4) == "alse") {
                        next();
                        return false;
                    }
                    break;
                case 'n':
                    if (read(3) == "ull") {
                        next();
                        return null;
                    }
                    break;
            }
            throw new SyntaxError("JSON.parse(): Invalid value at position " + pos + ".");
        }

        function value() {
            white();
            switch (c) {
                case '{':
                    return object();
                case '[':
                    return array();
                case '"':
                    return string();
                case '-':
                    return number();
                default:
                    return c >= '0' && c <= '9' ? number() : word();
            }
        }

        return value();
    
    };
    
    /**
     * Serializes a value into JSON, if possible.
     * @param {Variant} value The value to serialize.
     * @return {String} The serialized JSON string.
     */
    JSON.serialize = function(){
    
        var subs = {    // table of character substitutions
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        };
    
        var chars = /["\\\x00-\x1f\x7f-\x9f]/g;
        
        function f(num){
            return (num < 10) ? "0" + num : num;
        }
        
        return function(value /*:Variant*/) /*:String*/ {
            switch(typeof value){
                case "boolean":
                    return String(value);
                    
                case "number":
                    return isFinite(value) ? String(value) : "null";
                    
                case "string":
                
                    /*
                     * IE6 won't return true with the RegExp even if the string contains
                     * a newline character. The second test handles all of the special cases.
                     */
                    if (chars.test(value) || /[\n\b\t\f\r]/g.test(value)){
                    
                        value = value.replace(chars, function(match){
                            if (!subs[match]){
                                var c = match.charCodeAt(0);
                                subs[match] = "\\u00" + Math.floor(c / 16).toString(16) +
                                                       (c % 16).toString(16);
                            }
                            return subs[match];
                        });
                    }
                    
                    return "\"" + value + "\"";
                    
                case "object":
                    if (value !== null && typeof value.toJSONString == "function"){
                       return value.toJSONString();
                    } else if (value instanceof Date){
                        return value.getUTCFullYear() + '-' +
                            f(value.getUTCMonth() + 1) + '-' +
                            f(value.getUTCDate())      + 'T' +
                            f(value.getUTCHours())     + ':' +
                            f(value.getUTCMinutes())   + ':' +
                            f(value.getUTCSeconds())   + 'Z';
                    }
                default:
                    return "null";
            
            }
        };
    }();
    
    JSON.prototype = {
    
        //fix lost constructor
        constructor: JSON,
        
        /**
         * Runs a method on each item in the object.
         * @param {Function} method The method to run on each item.
         * @param {Object} scope (Optional) The scope in which to run the method.
         * @return {Void}
         */
        forEach : function(method /*:Function*/, scope /*:Object*/) /*:Void*/{
            if (typeof method != "function"){
                throw new Error("JSON.forEach(): First argument must be a function.");
            }
            
            for (var prop in this._data){
                method.call(scope, this._data[prop], prop, this);
            }
        },
        
        /**
         * Retrieves a value from the object.
         * @param {String} name The name of the value to retrieve.
         * @return {Variant} The value associated with the name or undefined if 
         *      there is no value associated with the name.
         */
        get : function(name /*:String*/)/*:Variant*/{
        
            if (typeof name != "string"){
                throw new TypeError("Value name must be a string.");
            }
            
            return this._data[name];
        },
        
        /**
         * Adds a value with the given name to the object.
         * @param {String} name The name of the value to add.
         * @param {Variant} value The value to assign.
         * @throws TypeError If the name or value is invalid.
         * @return {Void}
         */
        put : function(name /*:String*/, value /*:Variant*/) /*:Void*/ {
            
            if (typeof name != "string"){
                throw new TypeError("Value name must be a string.");
            }
        
            if (isSupportedValue(value)){
                this._data[name] = value;
            } else {
                throw new TypeError("Invalid value data type.");
            }
        },
        
        /**
         * Serializes the object into a JSON string.
         * @return {String} The JSON string representation of the object.
         */
        toJSONString : function()/*:String*/{
            var parts = [];
            for (var prop in this._data){
                if (isSupportedValue(this._data[prop])){
                    parts.push(JSON.serialize(prop) + ":" + JSON.serialize(this._data[prop]));
                }
            }
            return "{" + parts.join(",") + "}";   
        }
    };
    
    /**
     * Represents an ordered list of JSON values.
     * @param {Array} data An array of data to add to the JSONList.
     * @class JSONList
     * @constructor
     */
    JSONList = function(data /*:Array*/) {
        if (this instanceof JSONList){
            if (data instanceof Array){
                this._data = Array.prototype.slice.call(data, 0);
            } else {
                this._data = [];
            }
        } else {
            return new JSONList(data);
        }    
    };
    
    JSONList.prototype = function(){
        function F(){}
        F.prototype = JSON.prototype;
        return new F();
    }();
    
    /**
     * Retrieves the value at a given numeric index.
     * @param {int} index The position of the value to retrieve.
     * @return {Variant} The value at the given position or undefined if there isn't one.
     */
    JSONList.prototype.get = function(index /*:int*/) /*:Variant*/{
        if (typeof index != "number"){
            throw new TypeError("Index must be a number.");
        }
        
        return this._data[index];
    };
    
    /**
     * Returns the number of items in the object.
     * @return {int} The number of items in the object.
     */
    JSONList.prototype.getLength = function()/*:int*/{
        return this._data.length;
    };    
    
    /**
     * Runs a method on each item in the object.
     * @param {Function} method The method to run on each item.
     * @param {Object} scope (Optional) The scope in which to run the method.
     * @return {Void}
     */    
    JSONList.prototype.forEach = function(method, scope){
        if (typeof method != "function"){
            throw new Error("JSONList.forEach(): First argument must be a function.");
        }
        
        for (var i=0; i < this._data.length; i++){
            method.call(scope, this._data[i], i, this);
        }
    };
    
    /**
     * Removes the last item from the list and returns it.
     * @return {Variant} The last value in the list.
     */
    JSONList.prototype.pop = function()/*:Variant*/{
        return this._data.pop();
    };
    
    /**
     * Adds an item to the end of the list.
     * @param {Variant} value The value to add.
     * @return {int} The new length of the list.
     */
    JSONList.prototype.push = function(value /*:Variant*/){
        if (!isSupportedValue(value)){
            throw new TypeError("Invalid value data type.");
        }
        
        return this._data.push(value);
    };
    
    /**
     * Adds a value with the given name to the object.
     * @param {String} name The name of the value to add.
     * @param {Variant} value The value to assign.
     * @throws TypeError If the name or value is invalid.
     * @return {Void}
     */
    JSONList.prototype.put = function(index, value){
        if (typeof index != "number"){
            throw new TypeError("Index must be a number.");
        }
        
        if (isSupportedValue(value)){
            this._data[index] = value;
        } else {
            throw new TypeError("Invalid value data type.");
        }
    };
    
    /**
     * Removes the first item from the list and returns it.
     * @return {Variant} The first value in the list.
     */
    JSONList.prototype.shift = function(){
        return this._data.shift();
    };
    
    /**
     * Returns an array representation of the list.     
     * @return {Array} An array representation of the list.
     */
    JSONList.prototype.toArray = function()/*:Array*/{
        return this._data.slice(0);
    };
    
    /**
     * Serializes the object into a JSON string.
     * @return {String} The JSON string representation of the object.
     */
    JSONList.prototype.toJSONString = function(){
        var parts = [];
        for (var i=0, len=this._data.length; i < len; i++){
            parts[i] = JSON.serialize(this._data[i]);
        }
        return "[" + parts.join(",") + "]";
    };
    
    /**
     * Adds an item to the beginning of the list.
     * @param {Variant} value The value to add.
     * @return {int} The new length of the list.
     */
    JSONList.prototype.unshift = function(value){
        if (!isSupportedValue(value)){
            throw new TypeError("Invalid value data type.");
        }

        return this._data.unshift(value);
    };
    
})();
